﻿using System;
using System.Security.Cryptography;
using System.Text;

namespace MvcEngine.Core.Helpers
{
    /// <summary>
    /// Manages the creation and verification of hashed values, using MD5 algorithm for this.
    /// </summary>
    public class HashHelper : IHashHelper
    {
        private static Random random = new Random();

        // Define min and max salt sizes.
        private const int MinSaltSize = 4;
        private const int MaxSaltSize = 8;


        public static IHashHelper Current
        {
            get { return IoC.Resolve<IHashHelper>(); }
        }

        /// <summary>
        /// Generates a hash for the given plain text value and returns a
        /// base64-encoded result. Before the hash is computed, a random salt
        /// should be generated and passed in <paramref name="saltValue"/>. 
        /// </summary>
        /// <param name="plainText">
        /// Plaintext value to be hashed. The method does not check whether
        /// this parameter is null.
        /// </param>
        /// <param name="saltValue">
        /// Salt string. This parameter cannot be <see langword="null"/> or empty, in which case an <see cref="T:ArgumentException"/> is thrown.
        /// </param>
        /// <returns>
        /// Hash value formatted as a base64-encoded string.
        /// </returns>
        /// <exception cref="T:ArgumentException">
        /// <paramref name="saltValue"/> is either <see langword="null"/> or empty.
        /// </exception>
        public string ComputeHash(string plainText, string saltValue)
        {
            Guard.ThrowIfEmptyString(saltValue, "Salt value specified cannot be null or empty. You may call GenerateSaltString method prior to calling ComputeHash method in order to generate a salt value.", "saltValue");

            // Allocate array, which will hold plain text and salt.
            byte[] plainTextWithSaltBytes = Encoding.Unicode.GetBytes(String.Concat(plainText, saltValue));

            MD5 hash = new MD5CryptoServiceProvider();

            // Compute hash value of our plain text with appended salt.
            byte[] hashBytes = hash.ComputeHash(plainTextWithSaltBytes);
            string hashValue = Convert.ToBase64String(hashBytes);

            // Return the result.
            return hashValue;
        }

        /// <summary>
        /// Compares a hash of the specified plain text value to a given hash
        /// value. Plain text should be hashed with the same salt value as the original
        /// hash.
        /// </summary>
        /// <param name="plainText">
        /// Plain text to be verified against the specified hash. The function
        /// does not check whether this parameter is null.
        /// </param>
        /// <param name="hashValue">
        /// Base64-encoded hash value to verify <paramref name="plainText"/> through. 
        /// </param>
        /// <param name="saltValue">
        /// The same salt value as with the original hash. When this parameter is either <see langword="null"/> or
        /// an empty string, an <see cref="T:ArgumentException"/> is thrown.
        /// </param>
        /// <returns>
        /// <see langword="true"/> if computed hash matches the specified <paramref name="hashValue"/>;
        /// otherwise, returns <see langword="false"/>.
        /// </returns>
        /// <exception cref="T:ArgumentException">
        /// <paramref name="saltValue"/> is either <see langword="null"/> or empty.
        /// </exception>
        public bool VerifyHash(string plainText, string hashValue, string saltValue)
        {
            Guard.ThrowIfEmptyString(saltValue, "Salt value specified cannot be null or empty. You may call GenerateSaltString method prior to calling ComputeHash method in order to generate a salt value.", "saltValue");

            // Compute a new hash string.
            string expectedHashString = ComputeHash(plainText, saltValue);

            // If the computed hash matches the specified hash,
            // the plain text value must be correct.
            return (hashValue == expectedHashString);
        }

        /// <summary>
        /// Generates a <see langword="string"/> of random and cryptografically strong sequence of characters.
        /// </summary>
        /// <returns>A <see langword="string"/> with minimum size of 4 and maximum size of 8 characters.</returns>
        public string GenerateHashString()
        {
            byte[] saltBytes = null;

            // Generate a random number for the size of the salt.
            int saltSize = random.Next(MinSaltSize, MaxSaltSize);

            // Allocate a byte array, which will hold the salt.
            saltBytes = new byte[saltSize];

            // Initialize a random number generator.
            RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

            // Fill the salt with cryptographically strong byte values.
            rng.GetNonZeroBytes(saltBytes);
            string salt = Convert.ToBase64String(saltBytes);

            return ComputeHash(salt, salt);
        }
    }
}
